﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.Data.Schema.ScriptDom;
using Microsoft.Data.Schema.ScriptDom.Sql;
using PropertyExpression.Common;

namespace PropertyExpression.ControllableQuery.Toolkit
{
    public static class QueryChecker
    {
        public static void CheckNonQueries(string connectionString, CheckQueryContext checkQueryContext, params INonQuery[] nonQueries)
        {
            CheckNonQueries(
                connectionString, checkQueryContext, new DoNothingQRepository(),
                nonQueries.Select(
                    nonQuery => new NonQueryHolder(nonQuery, query => new QLocation.NonDisplay())
                    ).ToArray()
                );
        }

        public static void CheckQueries<T>(string connectionString,
            QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, CheckQueryContext checkQueryContext,
            params IQuery<T>[] queries)
        {
            CheckQueries(
                connectionString, queryResultPropertyTypeChecker, checkQueryContext, new DoNothingQRepository(),
                queries.Select(
                    query => new QueryHolder<T>(query, query2 => new QLocation.NonDisplay())
                    ).ToArray()
                );
        }

        private static void CheckNonQueries(
            string connectionString,
            CheckQueryContext checkQueryContext, IQRepository qRepository, params NonQueryHolder[] nonQueryHolders)
        {
            foreach (var nonQueryHolder in nonQueryHolders)
                ExecuteQuery(connectionString, nonQueryHolder.ToQueryHolder(), delegate { },
                    checkQueryContext, qRepository, new Func<string, string>[] {});
        }

        private static void CheckQueries<T>(string connectionString,
            QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, CheckQueryContext checkQueryContext,
            IQRepository qRepository, params QueryHolder<T>[] queryHolders)
        {
            var genericArguments = typeof(T).GetGenericArguments();
            var attemptFuncs = GetAttemptFuncs(genericArguments);
            foreach (var queryHolderFunc in queryHolders)
                ExecuteQuery(connectionString, queryHolderFunc.ToQueryHolder(),
                    (sqlDataReader, i, queryHolder) => {
                        var schemaTable = sqlDataReader.GetSchemaTable();
                        var dataRows = schemaTable.Rows.Cast<DataRow>();
                        var genericArgument = genericArguments[i];
                        //TODO: добавить другие типы
                        if (genericArgument == typeof(int?) || genericArgument == typeof(Guid) || genericArgument == typeof(string)
                            || genericArgument == typeof(decimal?))
                        {
                            var dataRow = dataRows.Single();
                            queryResultPropertyTypeChecker(
                                genericArgument, dataRow, () => queryHolder.GetQueryName());
                        }
                        else
                        {
                            var propertyInfos = genericArgument.GetProperties();
                            var propertyInfoDictionary = propertyInfos.ToDictionary(propertyInfo => propertyInfo.Name);
                            foreach (var dataRow in dataRows)
                                propertyInfoDictionary.Get(dataRow.GetColumnName()).
                                    Match(
                                        propertyInfo => queryResultPropertyTypeChecker(
                                            propertyInfo.PropertyType, dataRow, () => queryHolder.GetQueryName()),
                                        () => {
                                            throw new QueryCheckException(
                                                string.Format(
                                                    @"The query result type does not contain a definition for property '{0}'.
QueryName = '{1}'",
                                                    dataRow.GetColumnName(),
                                                    queryHolder.GetQueryName()));
                                        }
                                    );
                            var schemaTableDictionary = dataRows.ToDictionary(_ => _.GetColumnName());
                            foreach (var propertyInfo in propertyInfos)
                                if (!schemaTableDictionary.Get(propertyInfo.Name).HasValue)
                                    throw new QueryCheckException(
                                        string.Format(
                                            @"The query result does not contain a column '{0}'.
QueryType = '{1}'",
                                            propertyInfo.Name,
                                            queryHolder.GetQueryName()));
                        }
                    },
                    checkQueryContext,
                    qRepository,
                    attemptFuncs);
        }

        private static void ExecuteQuery(
            string connectionString, QueryHolder queryHolder,
            Action<SqlDataReader, int, QueryHolder> result, CheckQueryContext checkQueryContext,
            IQRepository qRepository, IEnumerable<Func<string, string>> attemptFuncs)
        {
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
                using (var command = new SqlCommand())
                {
                    command.Connection = connection;
                    var queryText = queryHolder.Query.CommandText;
                    command.CommandType = queryHolder.Query.CommandType;
                    queryHolder.Query.AddParameters(command);
                    if (checkQueryContext.IsAlreadyChecked(queryText, command)) return;
                    checkQueryContext.AddQueryText(queryText, command);

                    foreach (var commandTextFunc in attemptFuncs.Concat(new Func<string, string>[] {_ => _}))
                    {
                        command.CommandText = commandTextFunc(queryText);
                        SqlDataReader reader;
                        try
                        {
                            reader = command.ExecuteReader(CommandBehavior.SchemaOnly);
                        }
                        catch (SqlException exception)
                        {
                            if (command.CommandText != queryText) continue;
                            throw new QueryCheckException(command.CommandText, exception, queryHolder.GetQueryName());
                        }
                        using (reader)
                        {
                            var i = 0;
                            //TODO: если в Query стоит несколько типов, а запрос возвращает один результат, то нет сообщения об ошибке
                            do result(reader, i++, queryHolder); while (reader.NextResult());
                        }
                        qRepository.Add(command, queryHolder.GetQLocation());
                        return;
                    }
                    throw new InvalidOperationException();
                }
            }
        }

        private static IEnumerable<Func<string, string>> GetAttemptFuncs(Type[] genericArguments)
        {
            if (genericArguments.Length == 1)
                yield return InsertFalseCondition;
            if (!Debugger.IsAttached) //because TSql100Parser is very slow when debugger is attached
                yield return InsertFalseCondition2;
        }

        private static string InsertFalseCondition(string queryText)
        {
            return "SELECT * FROM (" + queryText + ") _ WHERE 0 = 1";
        }

        /// <summary>
        /// //TODO: вызывать только если MS SQL
        /// </summary>
        private static string InsertFalseCondition2(string queryText)
        {
            var parser = new TSql100Parser(false);
            IList<ParseError> parseErrors;
            var scriptFragment = parser.Parse(new StringReader(queryText), out parseErrors);
            if (parseErrors.Count > 0) return queryText;
            if (!(scriptFragment is TSqlScript)) return queryText;
            var actions = new List<Action<StringBuilder>>();
            foreach (var sqlBatch in ((TSqlScript)scriptFragment).Batches)
            {
                foreach (var sqlStatement in sqlBatch.Statements)
                {
                    if (!(sqlStatement is SelectStatement)) continue;
                    var selectStatement = ((SelectStatement)sqlStatement).QueryExpression;
                    if (!(selectStatement is QuerySpecification)) continue;
                    var querySpecification = (QuerySpecification)selectStatement;
                    var whereClause = querySpecification.WhereClause;
                    if (whereClause == null)
                    {
                        var fromClause = querySpecification.FromClauses.LastOrDefault();
                        if (fromClause == null)
                            continue;
                        actions.Add(builder => builder.Insert(
                            fromClause.StartOffset + fromClause.FragmentLength, " WHERE 0 = 1 "));
                    }
                    else
                    {
                        var sqlParserToken = whereClause.ScriptTokenStream[whereClause.FirstTokenIndex];
                        if (sqlParserToken.TokenType != TSqlTokenType.Where) continue;

                        actions.Add(builder => {
                            builder.Insert(
                                whereClause.StartOffset + whereClause.FragmentLength, " ) ");
                            builder.Insert(
                                sqlParserToken.Offset + sqlParserToken.Text.Length, " 0 = 1 AND ( ");
                        });
                    }
                }
            }
            actions.Reverse();
            var stringBuilder = new StringBuilder(queryText);
            foreach (var action in actions) action(stringBuilder);
            return stringBuilder.ToString();
        }

        public static void CheckAllQueries(
            string connectionString,
            QueryResultPropertyTypeChecker queryResultPropertyTypeChecker,
            ParamCreator paramCreator,
            Func<Type, bool> queryTypePredicate,
            Func<MethodInfo, bool> methodInfoPredicate,
            ChoiceInvocationGetter choiceInvocationGetter,
            IEnumerable<Type> types,
            IQRepository qRepository)
        {
            var checkQueryContext = new CheckQueryContext();
            foreach (var type in types)
            {
                if (!QueryExtensions.QueryImplementationType.Equals(type)
                    && !QueryExtensions.NonQueryImplementationType.Equals(type))
                {
                    foreach (var @interface in type.GetInterfaces())
                    {
                        if (@interface.IsGenericTypeAndEquals(typeof(IQuery<>)))
                        {
                            if (queryTypePredicate(type))
                            {
                                //TODO: WriteLine
                                //Console.WriteLine(type);
                                foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter).GetConstructorInvocations(type))
                                {
                                    InvokeCheckQuery(connectionString, queryResultPropertyTypeChecker, paramCreator,
                                        queryCreator, @interface.GetGenericArguments(),
                                        query => new QLocation.Class(query), checkQueryContext, qRepository,
                                        checkQueryMethodInfo);
                                }
                            }
                        }
                        if (typeof(INonQuery).Equals(@interface))
                        {
                            if (queryTypePredicate(type))
                            {
                                //TODO: WriteLine
                                //Console.WriteLine(type);
                                foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter).GetConstructorInvocations(type))
                                {
                                    InvokeCheckNonQuery(connectionString, paramCreator, queryCreator,
                                        query => new QLocation.Class(query), checkQueryContext, qRepository);
                                }
                            }
                        }
                    }
                }
                foreach (var methodInfo in type.GetMethods(ControllableQuery.Util.AllBindingFlags))
                    if (methodInfoPredicate(methodInfo))
                    {
                        Func<MethodInfo, Action<MethodInfo>> action = aCheckQueryMethodInfo =>
                            resolveMethodInfo =>
                                CheckMethod(methodInfo, choiceInvocationGetter,
                                    queryCreator =>
                                        InvokeCheckQuery(connectionString, queryResultPropertyTypeChecker,
                                            paramCreator, queryCreator,
                                            resolveMethodInfo.GetGenericArguments(),
                                            query => new QLocation.Method(methodInfo), checkQueryContext,
                                            qRepository, aCheckQueryMethodInfo));
                        Func<MethodInfo, MethodInfo, Action<MethodInfo, List<ILInstruction>>> onNewQuery =
                            (aCheckQueryMethodInfo, aQueryInternalMethodInfo) =>
                                (resolveMethodInfo, ilInstructions) => {
                                    string filePath;
                                    int lineNumber;
                                    CommandType commandType;
                                    string queryText;
                                    ILProcessor.QueryInfo(ilInstructions, out filePath, out lineNumber, out commandType,
                                        out queryText);
                                    Save(filePath, lineNumber, queryText);
                                    //TODO: перенести NewInternal в не дженерик класс
                                    var genericArguments = resolveMethodInfo.DeclaringType.GetGenericArguments();
                                    var info = aQueryInternalMethodInfo.MakeGenericMethod(
                                        genericArguments[0],
                                        ControllableQuery.Util.ParamInferenceType(
                                            resolveMethodInfo.GetGenericArguments()[0])
                                        );
                                    RuntimeHelpers.RunClassConstructor(methodInfo.DeclaringType.TypeHandle);
                                    foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter)
                                        .GetMethodBaseInvocations(
                                            new[] {info.GetParameters()[0]},
                                            args => info.Invoke(null,
                                                args.Concat(new object[] {
                                                    queryText, commandType, lineNumber, filePath
                                                }).ToArray())))
                                        InvokeCheckQuery(connectionString, queryResultPropertyTypeChecker,
                                            paramCreator, queryCreator,
                                            genericArguments,
                                            //TODO: указывать более точно QLocation
                                            query => new QLocation.StringQuery(filePath, lineNumber), checkQueryContext,
                                            qRepository, aCheckQueryMethodInfo);
                                };
                        FindMethod(
                            methodInfo,
                            methodInfo.GetGenericArguments,
                            query: action(checkQueryMethodInfo),
                            newQuery: onNewQuery(checkQueryMethodInfo, queryInternalMethodInfo),
                            newBatchQuery: onNewQuery(batchCheckQueryMethodInfo, batchQueryInternalMethodInfo),
                            newNonQuery: (resolveMethodInfo, ilInstructions) => {
                                string filePath;
                                int lineNumber;
                                CommandType commandType;
                                string queryText;
                                ILProcessor.QueryInfo(ilInstructions, out filePath, out lineNumber, out commandType,
                                    out queryText);
                                Save(filePath, lineNumber, queryText);
                                //TODO: перенести NewInternal в не дженерик класс
                                var info = nonQueryInternalMethodInfo.MakeGenericMethod(
                                    ControllableQuery.Util.ParamInferenceType(
                                        resolveMethodInfo.GetGenericArguments()[0]));
                                RuntimeHelpers.RunClassConstructor(methodInfo.DeclaringType.TypeHandle);
                                foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter)
                                    .GetMethodBaseInvocations(
                                        new[] { info.GetParameters()[0] },
                                        args => info.Invoke(null,
                                            args.Concat(new object[] {
                                                    queryText, commandType, lineNumber, filePath
                                                }).ToArray())))
                                    InvokeCheckNonQuery(connectionString, paramCreator, queryCreator,
                                        //TODO: указывать более точно QLocation
                                        query => new QLocation.StringQuery(filePath, lineNumber), checkQueryContext,
                                        qRepository);                                
                            },
                            query2: action(checkQuery2MethodInfo),
                            multipleQuery: action(batchCheckQueryMethodInfo),
                            tuple2Query: action(tuple2CheckQueryMethodInfo),
                            nonQuery: resolveMethodInfo => CheckMethod(
                                methodInfo, choiceInvocationGetter,
                                queryCreator => InvokeCheckNonQuery(connectionString, paramCreator, queryCreator,
                                    query => new QLocation.Method(methodInfo), checkQueryContext,
                                    qRepository)),
                            queryRecordExample: action(checkQueryMethodInfo),
                            @select: action(checkQueryMethodInfo));
                    }
                foreach (var methodInfo in type.GetConstructors(ControllableQuery.Util.AllBindingFlags))
                    FindMethod(methodInfo, () => new Type[] {},
                        resolveMethodInfo => ThrowQueryCheckException(queryMethodInfo, methodInfo),
                        delegate { ThrowQueryCheckException(ILProcessor.NewQueryMethodInfo, methodInfo); },
                        delegate { ThrowQueryCheckException(ILProcessor.newBatchQueryMethodInfo, methodInfo); },
                        delegate { ThrowQueryCheckException(ILProcessor.NewNonQueryMethodInfo, methodInfo); },
                        resolveMethodInfo => ThrowQueryCheckException(query2MethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(batchQueryMethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(tuple2QueryMethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(nonQueryMethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(queryRecordExampleMethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(selectMethodInfo, methodInfo));
            }
        }

        private static void Save(string filePath, int lineNumber, string queryText)
        {
            var queryPath = ControllableQuery.Util.GetQueryPath(filePath, lineNumber);
            if (!File.Exists(queryPath) || File.ReadAllText(queryPath) != queryText)
                File.WriteAllText(queryPath, queryText);
        }

        private static void CheckMethod(
            MethodInfo methodInfo,
            ChoiceInvocationGetter choiceInvocationGetter, 
            Action<QueryCreator> action)
        {
            if (!methodInfo.IsStatic)
            {
                throw new QueryCheckException(string.Format("Method is not static Method = '{0}', DeclaringType = '{1}'.", methodInfo, methodInfo.DeclaringType));
            }
            //TODO: WriteLine
            //Console.WriteLine(methodInfo);
            foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter)
                .GetStaticMethodInvocations(methodInfo))
            {
                action(queryCreator);
            }
        }

        private static void ThrowQueryCheckException(MethodBase methodInfo, ConstructorInfo constructorInfo)
        {
            throw new QueryCheckException(
                string.Format("{0} method is called in a constructor of type '{1}'.",
                              methodInfo.Name, constructorInfo.DeclaringType));
        }

        private static void InvokeCheckNonQuery(string connectionString, ParamCreator paramCreator,
                                                QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc,
                                                CheckQueryContext checkQueryContext,
                                                IQRepository qRepository)
        {
            checkNonQueryMethodInfo.Invoke(null, new object[] { connectionString, paramCreator, queryCreator, qLocationFunc, checkQueryContext, qRepository });
        }

        private static void InvokeCheckQuery(string connectionString,
            QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator,
            QueryCreator queryCreator, Type[] getGenericArguments, Func<IQuery, QLocation> qLocationFunc,
            CheckQueryContext checkQueryContext, IQRepository qRepository, MethodInfo aCheckQueryMethodInfo)
        {
            aCheckQueryMethodInfo.MakeGenericMethod(getGenericArguments)
                .Invoke(null, new object[] { connectionString, queryResultPropertyTypeChecker, paramCreator, queryCreator, qLocationFunc, checkQueryContext, qRepository });
        }

        private static readonly MethodInfo nonQueryMethodInfo =
            QWriter.GetMethodInfo<Func<QWriter, INonQuery>>(_ => _.NonQuery());

        private static readonly MethodInfo queryMethodInfo =
            QWriter.GetMethodInfo<Func<QWriter, IQuery<Type<object>>>>(_ => _.Query<object>())
                .GetGenericMethodDefinition();

        private static readonly MethodInfo queryInternalMethodInfo =
            QWriter.GetMethodInfo<Func<string, object, CommandType, int, string, IQuery<Type<object>>>>(
                (x1, x2, x3, x4, x5) => ControllableQuery.Util.Query<object, object>(x2, x1, x3, x4, x5))
                .GetGenericMethodDefinition();

        private static readonly MethodInfo batchQueryInternalMethodInfo =
            QWriter.GetMethodInfo<Func<string, object, CommandType, int, string, IQuery<object>>>(
                (x1, x2, x3, x4, x5) => ControllableQuery.Util.BatchQuery<object, object>(x2, x1, x3, x4, x5))
                .GetGenericMethodDefinition();

        private static readonly MethodInfo nonQueryInternalMethodInfo =
            QWriter.GetMethodInfo<Func<string, object, CommandType, int, string, INonQuery>>(
                (x1, x2, x3, x4, x5) => ControllableQuery.Util.NonQuery(x2, x1, x3, x4, x5))
                .GetGenericMethodDefinition();

        private static readonly MethodInfo query2MethodInfo =
            QWriter.GetMethodInfo<Func<QWriter, IQuery<Type<object, object>>>>(_ => _.Query<object, object>())
                .GetGenericMethodDefinition();

        private static readonly MethodInfo batchQueryMethodInfo =
            QWriter.GetMethodInfo<Func<QWriter, IQuery<object>>>(
                _ => _.BatchQuery<object>()).GetGenericMethodDefinition();

        private static readonly MethodInfo tuple2QueryMethodInfo =
            QWriter.GetMethodInfo<Func<Tuple<QWriter, QWriter>, Tuple<IQuery<object>, IQuery<object>>>>(
                _ => _.BatchQuery<object, object>()).GetGenericMethodDefinition();

        private static readonly MethodInfo queryRecordExampleMethodInfo =
            QWriter.GetMethodInfo<Func<QWriter, object, IQuery<Type<object>>>>((x1, x2) => x1.Query(x2)).GetGenericMethodDefinition();

        private static readonly MethodInfo selectMethodInfo =
            QWriter.GetMethodInfo<Func<QWriter, Func<ColumnSelector, object>, IQuery<Type<object>>>>((x1, x2) => x1.Select(x2)).GetGenericMethodDefinition();

        private delegate void Action<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);

        private static readonly MethodInfo checkNonQueryMethodInfo =
            QWriter.GetMethodInfo<Action<string, ParamCreator, QueryCreator, Func<IQuery, QLocation>, CheckQueryContext, IQRepository>>((x1, x2, x3, x4, x5, x6) => CheckNonQuery(x1, x2, x3, x4, x5, x6));

        private static void CheckNonQuery(string connectionString, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, CheckQueryContext checkQueryContext, IQRepository qRepository)
        {
            CheckNonQueries(connectionString, checkQueryContext, qRepository, new NonQueryHolder(GetQueryFuncs<INonQuery>(queryCreator, paramCreator), qLocationFunc));
        }

        private delegate void Action<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);

        private static readonly MethodInfo checkQueryMethodInfo =
            QWriter.GetMethodInfo<Action<string, QueryResultPropertyTypeChecker, ParamCreator, QueryCreator, Func<IQuery, QLocation>, CheckQueryContext, IQRepository>>(
            (x1, x2, x3, x4, x5, x6, x7) => CheckQuery<object>(x1, x2, x3, x4, x5, x6, x7)).GetGenericMethodDefinition();

        private static void CheckQuery<T>(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, CheckQueryContext checkQueryContext, IQRepository qRepository)
        {
            CheckQueries(connectionString, queryResultPropertyTypeChecker, checkQueryContext, qRepository,
                new QueryHolder<Type<T>>(GetQueryFuncs<IQuery<Type<T>>>(queryCreator, paramCreator), qLocationFunc));
        }

        private static readonly MethodInfo checkQuery2MethodInfo = 
            QWriter.GetMethodInfo<Action<string, QueryResultPropertyTypeChecker, ParamCreator, QueryCreator, Func<IQuery, QLocation>, CheckQueryContext, IQRepository>>(
            (x1, x2, x3, x4, x5, x6, x7) => CheckQuery2<object, object>(x1, x2, x3, x4, x5, x6, x7)).GetGenericMethodDefinition();

        private static void CheckQuery2<T1, T2>(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, CheckQueryContext checkQueryContext, IQRepository qRepository)
        {
            CheckQueries(connectionString, queryResultPropertyTypeChecker, checkQueryContext, qRepository,
                new QueryHolder<Type<T1, T2>>(GetQueryFuncs<IQuery<Type<T1, T2>>>(queryCreator, paramCreator), qLocationFunc));
        }

        private static readonly MethodInfo tuple2CheckQueryMethodInfo =
            QWriter.GetMethodInfo<Action<string, QueryResultPropertyTypeChecker, ParamCreator, QueryCreator, Func<IQuery, QLocation>, CheckQueryContext, IQRepository>>(
            (x1, x2, x3, x4, x5, x6, x7) => CheckQuery<object, object>(x1, x2, x3, x4, x5, x6, x7)).GetGenericMethodDefinition();

        private static void CheckQuery<T1, T2>(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, CheckQueryContext checkQueryContext, IQRepository qRepository)
        {
            var tuple = GetQueryFuncs<Tuple<IQuery<T1>, IQuery<T2>>>(queryCreator, paramCreator);
            CheckQueries(connectionString, queryResultPropertyTypeChecker, checkQueryContext, qRepository, 
                new QueryHolder<T1>(tuple.Item1, qLocationFunc));
            CheckQueries(connectionString, queryResultPropertyTypeChecker, checkQueryContext, qRepository,
                new QueryHolder<T2>(tuple.Item2, qLocationFunc));
        }

        private static readonly MethodInfo batchCheckQueryMethodInfo =
            QWriter.GetMethodInfo<Action<string, QueryResultPropertyTypeChecker, ParamCreator, QueryCreator, Func<IQuery, QLocation>, CheckQueryContext, IQRepository>>(
            (x1, x2, x3, x4, x5, x6, x7) => BatchCheckQuery<object>(x1, x2, x3, x4, x5, x6, x7)).GetGenericMethodDefinition();

        private static void BatchCheckQuery<T>(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, CheckQueryContext checkQueryContext, IQRepository qRepository)
        {
            CheckQueries(connectionString, queryResultPropertyTypeChecker, checkQueryContext, qRepository,
                new QueryHolder<T>(GetQueryFuncs<IQuery<T>>(queryCreator, paramCreator), qLocationFunc));
        }

        private static T GetQueryFuncs<T>(QueryCreator queryCreator, ParamCreator paramCreator)
        {
            return (T) queryCreator(new QueryCreatorContext(type => paramCreator(type)));
        }

        private static IEnumerable<IEnumerable<TResult>> GetAllChoices<T, TResult>(
            IEnumerable<T> items,
            Func<T, IEnumerable<TResult>> choiceFunc)
        {
            return items.Aggregate(
                new IEnumerable<TResult>[] { },
                (seed, item) =>
                choiceFunc(item).SelectMany(
                    choiceItem => seed.Length == 0
                                          ? new[] { new[] { choiceItem } }
                                          : seed.Select(resultItem => resultItem.Concat(new[] { choiceItem }))
                    ).ToArray()
                );
        }

        private class ChoiceInvocationContext : IChoiceInvocationContext
        {
            private readonly ChoiceInvocationGetter choiceInvocationGetter;

            internal ChoiceInvocationContext(
                ChoiceInvocationGetter choiceInvocationGetter)
            {
                this.choiceInvocationGetter = choiceInvocationGetter;
            }

            public IEnumerable<QueryCreator> GetMethodBaseInvocations(ParameterInfo[] parameterInfos, Func<object[], object> invokeFunc)
            {
                if (parameterInfos.Length == 0)
                {
                    return new QueryCreator[] { delegate { return invokeFunc(new object[] { }); } };
                }
                IEnumerable<IEnumerable<QueryCreator>> result;
                if (parameterInfos.Any(CheckedAsParamGroup))
                {
                    result = parameterInfos.Where(CheckedAsParamGroup).SelectMany(
                        currentParameterInfo =>
                        GetAllChoices(
                            parameterInfos,
                            parameterInfo =>
                                {
                                    Func<IEnumerable<QueryCreator>, IEnumerable<QueryCreator>> func;
                                    if (parameterInfo.Equals(currentParameterInfo))
                                    {
                                        func = _ => _;
                                    }
                                    else
                                    {
                                        if (CheckedAsParamGroup(parameterInfo))
                                        {
                                            func = _ => new[] {_.First()};
                                        }
                                        else
                                        {
                                            func = _ => _;
                                        }
                                    }
                                    return func(GetChoiceInvocations(parameterInfo.ParameterType));
                                })
                            );
                }
                else
                {
                    result = GetAllChoices(
                        parameterInfos,
                        parameterInfo => GetChoiceInvocations(parameterInfo.ParameterType));
                }
                return result.Select<IEnumerable<QueryCreator>, QueryCreator>(
                    queryCreatorEnumerable => queryCreatorContext => invokeFunc(
                        queryCreatorEnumerable.Select(queryCreator => queryCreator(queryCreatorContext)).ToArray()));
            }

            private static bool CheckedAsParamGroup(ParameterInfo parameterInfo)
            {
                return parameterInfo.GetCustomAttributes(typeof (ParamGroupAttribute), true).Length > 0 ||
                       parameterInfo.ParameterType.GetCustomAttributes(typeof (ParamGroupAttribute), true).Length > 0;
            }

            private IEnumerable<QueryCreator> GetChoiceInvocations(Type type)
            {
                if (type.IsGenericTypeAndEquals(typeof(Option<>)))
                {
                    return new[] {asOptionalValueMethodInfo, nothingOptionalValueMethodInfo}.SelectMany(
                        genericMethodInfo => this.GetStaticMethodInvocations(genericMethodInfo.MakeGenericMethod(type.GetGenericArguments())));
                }
                else if (type.IsGenericTypeAndEquals(typeof (Nullable<>)))
                {
                    return new[] {createNullableMethod, nullCreateNullableMethod}.SelectMany(
                        genericMethodInfo => this.GetStaticMethodInvocations(genericMethodInfo.MakeGenericMethod(type.GetGenericArguments())));
                }
                else if (type.IsGenericTypeAndEquals(typeof (Param<>)))
                {
                    return new QueryCreator[] {
                        queryCreatorContext => queryCreatorContext.CreateParamFunc(type)
                    };
                }
                else if (typeof (bool).Equals(type))
                {
                    return new QueryCreator[] {delegate { return false; }, delegate { return true; }};
                }
                else if (type.IsGenericTypeAndEquals(typeof (Func<>)) && type.GetGenericArguments().Length == 1)
                {
                    var genericArgument = type.GetGenericArguments()[0];
                    var genericMethod = getFuncQueryCreatorMethodInfo.MakeGenericMethod(genericArgument);
                    return GetChoiceInvocations(genericArgument)
                        .Select(queryCreator => (QueryCreator) genericMethod.Invoke(
                            null, new object[] {queryCreator}));
                }
                else if (type.IsEnum)
                {
                    return Enum.GetValues(type).Cast<object>()
                        .Select<object, QueryCreator>(enumItem => delegate { return enumItem; });
                }
                else if (type.GetCustomAttributes(typeof (ParamGroupAttribute), true).Length > 0)
                {
                    return this.GetConstructorInvocations(type);
                }
                else if (DynamicMaterializer.IsAnonymousType(type))
                {
                    return this.GetConstructorInvocations(type);
                }
                else
                {
                    return choiceInvocationGetter(type, this);
                }
            }
        }

        private static readonly MethodInfo createNullableMethod =
            QWriter.GetMethodInfo<Func<int, int?>>(_ => CreateNullable(_)).GetGenericMethodDefinition();

        private static T? CreateNullable<T>(T arg) where T: struct
        {
            return arg;
        }

        private static readonly MethodInfo nullCreateNullableMethod =
            QWriter.GetMethodInfo<Func<int?>>(() => CreateNullable<int>()).GetGenericMethodDefinition();

        private static T? CreateNullable<T>() where T : struct
        {
            return new T?();
        }

        private static readonly MethodInfo getFuncQueryCreatorMethodInfo =
            QWriter.GetMethodInfo<Func<QueryCreator, QueryCreator>>(_ => GetFuncQueryCreator<object>(_)).GetGenericMethodDefinition();

        private static QueryCreator GetFuncQueryCreator<T>(QueryCreator queryCreator)
        {
            return context => Func.New(() => (T) queryCreator(context));
        }

        private static readonly MethodInfo asOptionalValueMethodInfo = 
            QWriter.GetMethodInfo<Func<object, Option<object>>>(_ => _.AsOption()).GetGenericMethodDefinition();

        private static readonly MethodInfo nothingOptionalValueMethodInfo =
            QWriter.GetMethodInfo<Func<Option<object>>>(() => CreateOption<object>()).GetGenericMethodDefinition();

        private static Option<T> CreateOption<T>()
        {
            return new Option<T>();
        }

        public static IEnumerable<Type> GetImplementations(Type interfaceType, Type[] types)
        {
            foreach (var type in types)
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (interfaceType.IsGenericType)
                    {
                        if (@interface.IsGenericType)
                        {
                            if (interfaceType.GetGenericTypeDefinition().Equals(@interface.GetGenericTypeDefinition()))
                            {
                                var typeArguments = type.GetGenericArguments();
                                var dictionary = typeArguments.Select((type1, index) => new { type = type1, index })
                                    .ToDictionary(arg => arg.type, arg => arg.index);
                                var interfaceArguments = @interface.GetGenericArguments();
                                var inputInterfaceArguments = interfaceType.GetGenericArguments();
                                for (var index = 0; index < interfaceArguments.Length; index++)
                                {
                                    typeArguments[dictionary[interfaceArguments[index]]] = inputInterfaceArguments[index];
                                }
                                yield return type.MakeGenericType(typeArguments);
                            }
                        }
                    }
                    else
                    {
                        if (interfaceType.Equals(@interface))
                        {
                            yield return type;
                        }
                    }
                }
            }
        }


        private static void FindMethod(
            MethodBase methodInfo,
            Func<Type[]> genericMethodArgumentsFunc,
            Action<MethodInfo> query,
            Action<MethodInfo, List<ILInstruction>> newQuery,
            Action<MethodInfo, List<ILInstruction>> newBatchQuery,
            Action<MethodInfo, List<ILInstruction>> newNonQuery,
            Action<MethodInfo> query2,
            Action<MethodInfo> multipleQuery,
            Action<MethodInfo> tuple2Query,
            Action<MethodInfo> nonQuery,
            Action<MethodInfo> queryRecordExample,
            Action<MethodInfo> @select)
        {
            var methodBody = methodInfo.GetMethodBody();
            if (methodBody == null) return;
            var ilAsByteArray = methodBody.GetILAsByteArray();
            var module = methodInfo.Module;
            //TODO: delete
            Action onResultAction = () => { };
            ILProcessor.ProcessMethodBody(
                ilAsByteArray,
                module,
                inlineMethod:
                    (metadataToken, position, ilInstructions) => {
                        var resolveMember = module.ResolveMember(
                            metadataToken,
                            methodInfo.DeclaringType.GetGenericArguments(),
                            genericMethodArgumentsFunc());
                        switch (resolveMember.MemberType)
                        {
                            case MemberTypes.Method:
                                var resolveMethodInfo = (MethodInfo)resolveMember;
                                if (ILProcessor.qFragmentMethodInfo.Equals(resolveMethodInfo))
                                {
                                    var index = ilInstructions.Count - 1;
                                    if (index < 0) throw new ApplicationException(); //TODO: написать сообщение
                                    var filePath = ILProcessor.GetFilePath(ilInstructions[index]);
                                    index--;
                                    if (index < 0) throw new ApplicationException(); //TODO: написать сообщение
                                    var lineNumber = ILProcessor.GetLineNumber(ilInstructions[index]);
                                    index--;
                                    if (index < 0) throw new ApplicationException(); //TODO: написать сообщение
                                    var queryText = ILProcessor.GetQueryText(ilInstructions[index]);
                                    Save(filePath, lineNumber, queryText);
                                    return false;
                                }
                                if (resolveMethodInfo.IsGenericMethod &&
                                    queryMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => query(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                if (resolveMethodInfo.IsGenericMethod &&
                                    ILProcessor.NewNonQueryMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    OnQueryMethod(
                                        methodInfo, position, () => newNonQuery(resolveMethodInfo, ilInstructions), genericMethodArgumentsFunc,
                                        ilAsByteArray, module, false);
                                    return false;
                                }
                                if (resolveMethodInfo.DeclaringType != null &&
                                    resolveMethodInfo.DeclaringType.IsGenericType &&
                                    resolveMethodInfo.DeclaringType.GetGenericTypeDefinition() == typeof(Query<>) &&
                                    resolveMethodInfo.Name == ILProcessor.NewQueryMethodInfo.Name)
                                {
                                    OnQueryMethod(
                                        methodInfo, position, () => newQuery(resolveMethodInfo, ilInstructions),
                                        genericMethodArgumentsFunc, ilAsByteArray, module, false);
                                    return false;
                                }
                                if (resolveMethodInfo.DeclaringType != null &&
                                    resolveMethodInfo.DeclaringType.IsGenericType &&
                                    resolveMethodInfo.DeclaringType.GetGenericTypeDefinition() == typeof(BatchQuery<>) &&
                                    resolveMethodInfo.Name == ILProcessor.newBatchQueryMethodInfo.Name)
                                {
                                    OnQueryMethod(
                                        methodInfo, position, () => newBatchQuery(resolveMethodInfo, ilInstructions),
                                        genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return false;
                                }
                                if (resolveMethodInfo.IsGenericMethod &&
                                    query2MethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => query2(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                if (resolveMethodInfo.IsGenericMethod &&
                                    batchQueryMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => multipleQuery(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                if (resolveMethodInfo.IsGenericMethod &&
                                    tuple2QueryMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => tuple2Query(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                if (resolveMethodInfo.IsGenericMethod &&
                                    queryRecordExampleMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => queryRecordExample(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                if (resolveMethodInfo.IsGenericMethod &&
                                    selectMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => @select(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                if (nonQueryMethodInfo.Equals(resolveMethodInfo))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => nonQuery(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                break;
                        }
                        return false;
                    },
                inlineField: delegate { },
                inlineTok: delegate { },
                inlineType: delegate { });
            onResultAction();
        }

        private static void OnQueryMethod(
            MethodBase methodInfo,
            int position,
            Action action,
            Func<Type[]> genericMethodArgumentsFunc,
            byte[] ilAsByteArray,
            Module module,
            bool checkAmbiguous = true)
        {
            ILProcessor.ProcessMethodBody(
                ilAsByteArray,
                module,
                inlineMethod:
                    (metadataToken, position2, ilInstructions) => {
                        if (checkAmbiguous && position != position2)
                        {
                            CheckMethod(
                                methodInfo,
                                module.ResolveMember(
                                    metadataToken,
                                    methodInfo.DeclaringType.GetGenericArguments(),
                                    genericMethodArgumentsFunc()));
                        }
                        return false;
                    },
                inlineField:
                    metadataToken => CheckType(
                        methodInfo,
                        module.ResolveField(
                            metadataToken,
                            methodInfo.DeclaringType.GetGenericArguments(),
                            genericMethodArgumentsFunc()).FieldType),
                inlineTok:
                    metadataToken => {
                        Type resolveType;
                        try
                        {
                            resolveType = module.ResolveType(
                                metadataToken,
                                methodInfo.DeclaringType.GetGenericArguments(),
                                genericMethodArgumentsFunc());
                        }
                        catch
                        {
                            FieldInfo resolveField;
                            try
                            {
                                resolveField = module.ResolveField(
                                    metadataToken,
                                    methodInfo.DeclaringType.GetGenericArguments(),
                                    genericMethodArgumentsFunc());
                            }
                            catch
                            {
                                MethodBase resolveMember;
                                try
                                {
                                    resolveMember = module.ResolveMethod(
                                        metadataToken,
                                        methodInfo.DeclaringType.GetGenericArguments(),
                                        genericMethodArgumentsFunc());
                                }
                                catch
                                {
                                    throw new QueryCheckException(
                                        string.Format(
                                            "The query checking cannot process method: '{0}'.",
                                            QLocationExtensions.GetQLocationString(methodInfo)));
                                }
                                CheckMethod(methodInfo, resolveMember);
                                return;
                            }
                            CheckType(methodInfo, resolveField.FieldType);
                            return;
                        }
                        CheckType(methodInfo, resolveType);
                    },
                inlineType:
                    metadataToken => CheckType(
                        methodInfo,
                        module.ResolveType(
                            metadataToken,
                            methodInfo.DeclaringType.GetGenericArguments(),
                            genericMethodArgumentsFunc()))
                );
            action();
        }

        private static void CheckMethod(MethodBase methodInfo, MemberInfo resolveMember)
        {
            switch (resolveMember.MemberType)
            {
                case MemberTypes.Method:
                    CheckType(methodInfo, ((MethodInfo)resolveMember).ReturnType);
                    break;
                case MemberTypes.Constructor:
                    CheckType(methodInfo, resolveMember.DeclaringType);
                    break;
            }
        }

        // ReSharper disable UnusedParameter.Local
        private static void CheckType(MethodBase methodInfo, Type type)
        // ReSharper restore UnusedParameter.Local
        {
            if (typeof(IQuery).IsAssignableFrom(type))
                throw new QueryCheckException(
                    string.Format(
                        "The instance of {0} is ambiguous: '{1}'.",
                        typeof(IQuery).Name,
                        QLocationExtensions.GetQLocationString(methodInfo)));
        }

        private static QueryHolder ToQueryHolder<T>(this QueryHolder<T> it)
        {
            return new QueryHolder(it.Query, it.QLocationFunc);
        }

        private static QueryHolder ToQueryHolder(this NonQueryHolder it)
        {
            return new QueryHolder(it.NonQuery, it.QLocationFunc);
        }

        private static QLocation GetQLocation(this QueryHolder it)
        {
            return it.QLocationFunc(it.Query);
        }

        private static string GetQueryName(this QueryHolder it)
        {
            return it.QLocationFunc(it.Query).Match(
                @class => @class.Query.GetType().ToString(),
                method => GetQueryName(method.MethodInfo),
                nonDisplay => { throw new NotImplementedException(); },
                stringQuery => stringQuery.FullName);
        }

        private static string GetQueryName(MethodInfo methodInfo)
        {
            return string.Format("{0}, {1}, {2}", methodInfo.DeclaringType, methodInfo.Name, methodInfo);
        }

        private class QueryHolder
        {
            private readonly IQuery query;
            private readonly Func<IQuery, QLocation> qLocationFunc;

            public QueryHolder(IQuery query, Func<IQuery, QLocation> qLocationFunc)
            {
                this.query = query;
                this.qLocationFunc = qLocationFunc;
            }

            public Func<IQuery, QLocation> QLocationFunc
            {
                get { return qLocationFunc; }
            }

            public IQuery Query
            {
                get { return query; }
            }
        }

        private class QueryHolder<T>
        {
            private readonly IQuery<T> query;
            private readonly Func<IQuery, QLocation> qLocationFunc;

            public QueryHolder(IQuery<T> query, Func<IQuery, QLocation> qLocationFunc)
            {
                this.query = query;
                this.qLocationFunc = qLocationFunc;
            }

            public IQuery<T> Query
            {
                get { return query; }
            }

            public Func<IQuery, QLocation> QLocationFunc
            {
                get { return qLocationFunc; }
            }
        }

        private class NonQueryHolder
        {
            private readonly INonQuery nonQuery;
            private readonly Func<IQuery, QLocation> qLocationFunc;

            public NonQueryHolder(INonQuery nonQuery, Func<IQuery, QLocation> qLocationFunc)
            {
                this.nonQuery = nonQuery;
                this.qLocationFunc = qLocationFunc;
            }

            public INonQuery NonQuery
            {
                get { return nonQuery; }
            }

            public Func<IQuery, QLocation> QLocationFunc
            {
                get { return qLocationFunc; }
            }
        }
    }
}